/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package phd.dupenois.es.nplusn;

import java.util.ArrayList;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 *
 * @param <T>
 * @author mpd209
 */
public abstract class NplusNEngine<T extends Solution> implements NplusNInterface<T> {

    private Random randomNoise;
    private Vector<T> archive;
    private Vector<NplusNListener<T>> listeners;
    private int maximumGenerations;
    private int population;
    private int generation;
    private NotificationThread notificationThread;

    public NplusNEngine() {
        this.randomNoise = new Random(System.currentTimeMillis());
        this.maximumGenerations = 500;
        this.population = 10;
        this.listeners = new Vector<NplusNListener<T>>();
    }

    public void addListener(NplusNListener<T> listener){
        if(!this.listeners.contains(listener)) this.listeners.add(listener);
    }
    public void removeListener(NplusNListener<T> listener){
        if(this.listeners.contains(listener)) this.listeners.remove(listener);
    }
    public NplusNListener<T>[] getListeners(){
        return this.listeners.toArray(new NplusNListener[this.listeners.size()]);
    }

    private final Vector<T> generateAllMutations(){
        Vector<T> mutants = new Vector<T>();
        Iterator<T> archiveIter = getArchive().iterator();
        ExecutorService mutantThreadPool = Executors.newFixedThreadPool(10);
        ArrayList<MutationThread> runningThreads = new ArrayList<MutationThread>();
        MutationThread thread;
        while (archiveIter.hasNext()) {
            thread = new MutationThread((T)archiveIter.next());
            runningThreads.add(thread);
            mutantThreadPool.submit(thread);
        }
        boolean allFinished = false;
        boolean threadFinished;
        Iterator<MutationThread> runningThreadsIterator;
        //Wait for all to finish
        while(!allFinished){
            runningThreadsIterator = runningThreads.iterator();
            threadFinished = true;

            while(runningThreadsIterator.hasNext() && threadFinished){
                MutationThread threadBeingChecked = runningThreadsIterator.next();
                threadFinished = threadBeingChecked.isFinished();
            }
            allFinished = threadFinished;
            if(!allFinished){
                try{Thread.sleep(1);}catch(InterruptedException ex){}
            }
        }
        runningThreadsIterator = runningThreads.iterator();
         while(runningThreadsIterator.hasNext()){
            mutants.add(runningThreadsIterator.next().getMutant());
        }
        return mutants;
    }

    private class MutationThread implements Runnable{
        private T mutant;
        private T original;
        private boolean finished;
        public MutationThread(T original){
            this.original = original;
            this.finished = false;
        }

        public void run() {
            this.mutant = mutateSolution(original);
            fillResults(this.mutant);
            notifyListeners(NplusNEventType.MUTATION_GENERATED, new Object[]{this.mutant, getGeneration()});
            this.finished = true;
        }
        public boolean isFinished(){
            return this.finished;
        }
        public T getMutant(){
            return this.mutant;
        }
        
    }

    public final void run() {
        this.notificationThread = new NotificationThread();
        new Thread(this.notificationThread).start();
        notifyListeners(NplusNEventType.START, new Object[0]);
        archive = new Vector<T>();
        for (int i = 0; i < getPopulation(); i++) {
//                System.out.println("Creating random set "+i);
            getArchive().add(generateRandomSolution());
        }

        for (T solution : getArchive()) {
            ObjectiveResult[] results = computeResults(solution);
            for (ObjectiveResult result : results) {
                solution.setValue(result.getObjective().getName(), result.getValue());
            }
            notifyListeners(NplusNEventType.SOLUTION_ADDED, new Object[]{solution, -1});
        }
        
        Domination fitnessResult;
        Iterator<T> archiveIter;
        Iterator<T> comparisonSetIter;
        T solution;
        T comparison;
        Vector<T> mutants;
        Vector<T> newArchive;
        for (generation = 0; getGeneration() < maximumGenerations && getArchive().size() > 0; generation++) {
            notifyListeners(NplusNEventType.GENERATION_STARTED, new Object[]{generation});
//                try{
//                    Thread.sleep(PAUSE);
//                }catch(InterruptedException ex){
//
//                }
            //SEE IF WE CAN THREAD THIS
//            while (archiveIter.hasNext()) {
//                mutant = mutateSolution((T)archiveIter.next());
//                mutants.add(mutant);
//                //Add mutants to graph
//                fillResults(mutant);
//                notifyListeners(NplusNEventType.MUTATION_GENERATED, new Object[]{mutant, generation});
//            }
            mutants = generateAllMutations();

            archive.addAll(mutants);
            randomisePopulationOrder();
            newArchive = new Vector<T>();
            archiveIter = getArchive().iterator();
            while (newArchive.size() < getPopulation() && archiveIter.hasNext()) {
                solution = archiveIter.next();
                //If it doesn't get above baselines ignore it
                if(!solution.aboveBaselines()) continue;
                comparisonSetIter = getArchive().iterator();
                fitnessResult = null;
                while ((fitnessResult == null || !fitnessResult.isDominated()) && comparisonSetIter.hasNext()) {
                    comparison = comparisonSetIter.next();
                    if (comparison == solution || comparison.equals(solution)) {
                        continue;
                    }
                    fitnessResult = solution.compareTo(comparison);
                }
                //If not dominated add it to new population then remove it
                //from current
                if (!fitnessResult.isDominated()) {
                    newArchive.add(solution);
                    //if new notify
                    if(mutants.contains(solution)) {
                        notifyListeners(NplusNEventType.SOLUTION_ADDED, new Object[]{solution, generation});
                    }
                    archiveIter.remove();
                }
            }
//            System.out.println("Generation "+generation+" Non Dominated: "+newArchive.size());
            //if we haven't got enough change identifiers add random ones
            //from the set (second randomisation may not be necessary)
            //ONLY DO THIS IF NOT ON LAST GENERATION
            if (getGeneration() != (getMaximumGenerations() - 1)) {
                randomisePopulationOrder();
                archiveIter = getArchive().iterator();
                while (newArchive.size() < getPopulation()) {
                    solution = archiveIter.next();
                    newArchive.add(solution);
                    //if new notify
                    if(mutants.contains(solution)) {
                        notifyListeners(NplusNEventType.SOLUTION_ADDED, new Object[]{solution, generation});
                    }
                    archiveIter.remove();
                }
            }
            //Notify of removed solutions
            for (Solution solutionToRemove : getArchive()) {
                notifyListeners(NplusNEventType.SOLUTION_REMOVED,
                        new Object[]{solutionToRemove, generation});
            }
            //set current population to new population
            this.archive = newArchive;
//            System.out.println("Generation "+generation+" archive: "+getArchive().size());
            notifyListeners(NplusNEventType.GENERATION_FINISHED, new Object[]{generation});
        }
        notifyListeners(NplusNEventType.FINISH, new Object[]{getArchiveAsArray()});
        //Should stop gracefully after finishing current notification queue
        notificationThread.stop();

    }
    private final void randomisePopulationOrder(){
        Collections.shuffle(this.archive);
    }
    public final void fillResults(T solution) {
        if (archive.contains(solution)) {
            return ;
        }
        ObjectiveResult[] results = computeResults(solution);
        for (ObjectiveResult result : results) {
            solution.setValue(result.getObjective().getName(), result.getValue());
        }
    }

    public double addNoise(double val, double scale, double[] limits) {
        val += randomNoise.nextGaussian() * scale;
        if (limits != null && limits.length >= 1) {
            if (val < limits[0]) {
                val = limits[0];
            }
        }
        if (limits != null && limits.length == 2) {
            if (val > limits[1]) {
                val = limits[1];
            }
        }
        return val;
    }

    public int addNoise(int val, int scale, int[] limits) {
        val += ((int) Math.round(randomNoise.nextGaussian())) * scale;
        if (limits != null && limits.length >= 1) {
            if (val < limits[0]) {
                val = limits[0];
            }
        }
        if (limits != null && limits.length == 2) {
            if (val > limits[1]) {
                val = limits[1];
            }
        }
        return val;
    }

    /**
     * @return the maximumGenerations
     */
    public int getMaximumGenerations() {
        return maximumGenerations;
    }

    /**
     * @return the archive
     */
    public Vector<T> getArchive() {
        return archive;
    }
    /**
     * @return the archive
     */
    public Solution[] getArchiveAsArray() {
        return getArchive().toArray(new Solution[getArchive().size()]);
    }
    /**
     * @return the generation
     */
    public int getGeneration() {
        return generation;
    }

    /**
     * @param maximumGenerations the maximumGenerations to set
     */
    public void setMaximumGenerations(int maximumGenerations) {
        this.maximumGenerations = maximumGenerations;
    }

    /**
     * @return the population
     */
    public int getPopulation() {
        return population;
    }

    /**
     * @param population the population to set
     */
    public void setPopulation(int population) {
        this.population = population;
    }

    public void notifyListeners(NplusNEventType eventType, Object[] parameters){
        notificationThread.addEvent(eventType, parameters);
    }

    private class NotificationThread implements Runnable{
        private boolean running;
        private boolean stop;
        private Vector<NplusNEvent<T>> queue;
        private boolean locked;
        public NotificationThread(){
            queue = new Vector<NplusNEvent<T>>();
        }
        public void addEvent(NplusNEventType eventType, Object[] parameters){
            locked = true;
            queue.add(new NplusNEvent<T>(eventType, parameters));
            locked = false;
        }
        public void smallSleep(){
            try {Thread.sleep(0, 1);} catch (InterruptedException ex) {}
        }
        public void run() {
            this.running = true;
            this.stop = false;
            NplusNEvent event = null;
            NplusNListener<T>[] listenersQueue = null;
            boolean gotListeners;
            boolean gotEvent;
            //Run till both stop and empty queue
            while(!stop||queue.size()>0){
                if(queue.size()>0&&!locked){
                    gotEvent = false;
                    try{
                        event = queue.remove(0);
                        gotEvent = true;
                    } catch (ConcurrentModificationException e) {
                        smallSleep();
                    }
                    if(gotEvent){
                        gotListeners = false;
                        while(!gotListeners){
                            try{
                                listenersQueue = getListeners();
                                gotListeners = true;
                            } catch (ConcurrentModificationException e) {
                                smallSleep();
                            }
                        }
                        for (NplusNListener<T> listener : listenersQueue) {
                            try{
                                event.notify(listener);
                            } catch (Exception e) {
                                System.out.println("Notification Error: ["+e.getClass().getSimpleName()+"] - '"+e.getMessage()+"'");
                                e.printStackTrace();
                            }
                        }
                    }
                }else{
                    smallSleep();
                }
            }
            this.running = false;
        }
        public void stop(){
            this.stop = true;
        }
        public boolean isRunning(){
            return running;
        }
        public boolean isStopping(){
            return stop&&running;
        }

    }
}
